﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Microsoft.Practices.CompositeUI.Properties;
using Microsoft.Practices.CompositeUI.Services;
using Microsoft.Practices.CompositeUI.Utility;
using Microsoft.Practices.ObjectBuilder;

namespace Microsoft.Practices.CompositeUI.Collections
{
    /// <summary>
    /// 	Collection of services that are contained in a <see cref = "WorkItem" />.
    /// </summary>
    public class ServiceCollection : ICollection, IEnumerable<KeyValuePair<Type, object>>
    {
        #region Fields & Properties

        private readonly ILifetimeContainer _container;
        private readonly IReadWriteLocator _locator;
        private readonly IBuilder<BuilderStage> _builder;

        #endregion

        #region Collection Fields & Properties

        private readonly ServiceCollection _parentCollection;

        #endregion

        #region Constructors

        /// <summary>
        /// 	Initializes an instance of the <see cref = "ServiceCollection" /> class.
        /// </summary>
        /// <param name = "container">The lifetime container the collection will use</param>
        /// <param name = "locator">The locator the collection will use</param>
        /// <param name = "builder">The builder the collection will use</param>
        /// <param name = "parentCollection">The parent collection</param>
        public ServiceCollection(ILifetimeContainer container, IReadWriteLocator locator, IBuilder<BuilderStage> builder, ServiceCollection parentCollection)
        {
            _container = container;
            _locator = locator;
            _builder = builder;
            _parentCollection = parentCollection;
        }

        #endregion

        #region Events

        /// <summary>
        /// 	Fired whenever a new service is added to the container. For demand-add services, the event is
        /// 	fired when the service is eventually created.
        /// </summary>
        public event EventHandler<DataEventArgs<object>> Added;

        /// <summary>
        /// 	Fired whenever a service is removed from the container. When the services collection is disposed,
        /// 	the Removed event is not fired for the services in the container.
        /// </summary>
        public event EventHandler<DataEventArgs<object>> Removed;

        /// <summary>
        /// 	Called when a new service is added to the collection.
        /// </summary>
        /// <param name = "service">The service that was added.</param>
        protected virtual void OnAdded(object service)
        {
            var added = Added;
            if (added != null)
            {
                added(this, new DataEventArgs<object>(service));
            }
        }

        /// <summary>
        /// 	Called when a service is removed from the collection.
        /// </summary>
        /// <param name = "service">The service that was removed.</param>
        protected virtual void OnRemoved(object service)
        {
            var removed = Removed;
            if (removed != null)
            {
                removed(this, new DataEventArgs<object>(service));
            }
        }

        #endregion

        #region Implementation of IEnumerable

        /// <summary>
        /// 	Returns an enumerator that iterates through the collection.
        /// </summary>
        /// <returns>
        /// 	A <see cref = "T:System.Collections.Generic.IEnumerator`1" /> that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>1</filterpriority>
        public IEnumerator<KeyValuePair<Type, object>> GetEnumerator()
        {
            var seenTypes = new Dictionary<Type, object>();
            IReadableLocator currentLocator = _locator;
            do
            {
                foreach (var pair in currentLocator.Where(p => p.Key is DependencyResolutionLocatorKey))
                {
                    var key = (DependencyResolutionLocatorKey) pair.Key;
                    if (key.ID == null)
                    {
                        var type = key.Type;
                        if (!seenTypes.ContainsKey(type))
                        {
                            seenTypes[type] = String.Empty;
                            yield return new KeyValuePair<Type, object>(type, pair.Value);
                        }
                    }
                }
            }
            while ((currentLocator = currentLocator.ParentLocator) != null);
        }

        /// <summary>
        /// 	Returns an enumerator that iterates through a collection.
        /// </summary>
        /// <returns>
        /// 	An <see cref = "T:System.Collections.IEnumerator" /> object that can be used to iterate through the collection.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        IEnumerator IEnumerable.GetEnumerator()
        {
            return GetEnumerator();
        }

        #endregion

        #region Implementation of ICollection

        /// <summary>
        /// 	Copies the elements of the <see cref = "T:System.Collections.ICollection" /> to an <see cref = "T:System.Array" />, starting at a particular <see
        ///  	cref = "T:System.Array" /> index.
        /// </summary>
        /// <param name = "array">The one-dimensional <see cref = "T:System.Array" /> that is the destination of the elements copied from <see
        ///  	cref = "T:System.Collections.ICollection" />. The <see cref = "T:System.Array" /> must have zero-based indexing. </param>
        /// <param name = "index">The zero-based index in <paramref name = "array" /> at which copying begins. </param>
        /// <exception cref = "T:System.ArgumentNullException"><paramref name = "array" /> is null. </exception>
        /// <exception cref = "T:System.ArgumentOutOfRangeException"><paramref name = "index" /> is less than zero. </exception>
        /// <exception cref = "T:System.ArgumentException"><paramref name = "array" /> is multidimensional.-or- The number of elements in the source <see
        ///  	cref = "T:System.Collections.ICollection" /> is greater than the available space from <paramref name = "index" /> to the end of the destination <paramref
        ///  	name = "array" />. </exception>
        /// <exception cref = "T:System.ArgumentException">The type of the source <see cref = "T:System.Collections.ICollection" /> cannot be cast automatically to the type of the destination <paramref
        ///  	name = "array" />. </exception>
        /// <filterpriority>2</filterpriority>
        void ICollection.CopyTo(Array array, int index)
        {
            this.ToArray().CopyTo(array, index);
        }

        /// <summary>
        /// 	Gets the number of elements contained in the <see cref = "T:System.Collections.ICollection" />.
        /// </summary>
        /// <returns>
        /// 	The number of elements contained in the <see cref = "T:System.Collections.ICollection" />.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        int ICollection.Count
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets an object that can be used to synchronize access to the <see cref = "T:System.Collections.ICollection" />.
        /// </summary>
        /// <returns>
        /// 	An object that can be used to synchronize access to the <see cref = "T:System.Collections.ICollection" />.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        object ICollection.SyncRoot
        {
            get
            {
                throw new NotImplementedException();
            }
        }

        /// <summary>
        /// 	Gets a value indicating whether access to the <see cref = "T:System.Collections.ICollection" /> is synchronized (thread safe).
        /// </summary>
        /// <returns>
        /// 	true if access to the <see cref = "T:System.Collections.ICollection" /> is synchronized (thread safe); otherwise, false.
        /// </returns>
        /// <filterpriority>2</filterpriority>
        bool ICollection.IsSynchronized
        {
            get
            {
                return false;
            }
        }

        #endregion

        #region Add

        /// <summary>
        /// 	Adds a service.
        /// </summary>
        /// <typeparam name = "TService">The type under which to register the service.</typeparam>
        /// <param name = "serviceInstance">The service to register.</param>
        /// <exception cref = "ArgumentNullException">serviceInstance is null.</exception>
        /// <exception cref = "ArgumentException">A service of the given type is already registered.</exception>
        public void Add<TService>(TService serviceInstance)
        {
            Add(typeof (TService), serviceInstance);
        }

        /// <summary>
        /// 	Adds a service.
        /// </summary>
        /// <param name = "serviceType">The type under which to register the service.</param>
        /// <param name = "serviceInstance">The service to register.</param>
        /// <exception cref = "ArgumentNullException">serviceInstance is null.</exception>
        /// <exception cref = "ArgumentException">A service of the given type is already registered.</exception>
        public void Add(Type serviceType, object serviceInstance)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            Guard.ArgumentNotNull(serviceInstance, "serviceInstance");
            Build(serviceInstance.GetType(), serviceType, serviceInstance);
        }

        /// <summary>
        /// 	Adds a service that will not be created until the first time it is requested.
        /// </summary>
        /// <typeparam name = "TService">The type of service</typeparam>
        public void AddOnDemand<TService>()
        {
            AddOnDemand(typeof (TService), null);
        }

        /// <summary>
        /// 	Adds a service that will not be created until the first time it is requested.
        /// </summary>
        /// <typeparam name = "TService">The type of service</typeparam>
        /// <typeparam name = "TRegisterAs">The type to register the service as</typeparam>
        public void AddOnDemand<TService, TRegisterAs>()
        {
            AddOnDemand(typeof (TService), typeof (TRegisterAs));
        }

        /// <summary>
        /// 	Adds a service that will not be created until the first time it is requested.
        /// </summary>
        /// <param name = "serviceType">The type of service</param>
        public void AddOnDemand(Type serviceType)
        {
            AddOnDemand(serviceType, null);
        }

        /// <summary>
        /// 	Adds a service that will not be created until the first time it is requested.
        /// </summary>
        /// <param name = "serviceType">The type of service</param>
        /// <param name = "registerAs">The type to register the service as</param>
        public void AddOnDemand(Type serviceType, Type registerAs)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            if (registerAs == null)
            {
                registerAs = serviceType;
            }
            var key = new DependencyResolutionLocatorKey(registerAs, null);
            if (_locator.Contains(key, SearchMode.Local))
            {
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, Resources.DuplicateService, registerAs.FullName));
            }
            var placeholder = new DemandAddPlaceholder(serviceType);
            _locator.Add(key, placeholder);
            _container.Add(placeholder);
        }

        /// <summary>
        /// 	Creates and adds a service.
        /// </summary>
        /// <typeparam name = "TService">The type of the service to create. This type is also
        /// 	the type the service is registered under.</typeparam>
        /// <returns>The new service instance.</returns>
        /// <exception cref = "ArgumentException">Object builder cannot find an appropriate
        /// 	constructor on the object.</exception>
        public TService AddNew<TService>()
        {
            return AddNew<TService, TService>();
        }

        /// <summary>
        /// 	Creates a service.
        /// </summary>
        /// <typeparam name = "TService">The type of the service to create.</typeparam>
        /// <typeparam name = "TRegisterAs">The type the service is registered under.</typeparam>
        /// <returns>The new service instance.</returns>
        /// <exception cref = "ArgumentException">Object builder cannot find an appropriate
        /// 	constructor on the object.</exception>
        public TService AddNew<TService, TRegisterAs>() where TService : TRegisterAs
        {
            return (TService) AddNew(typeof (TService), typeof (TRegisterAs));
        }

        /// <summary>
        /// 	Creates a service.
        /// </summary>
        /// <param name = "serviceType">The type of the service to create. This type is also
        /// 	the type the service is registered under.</param>
        /// <returns>The new service instance.</returns>
        /// <exception cref = "ArgumentException">Object builder cannot find an appropriate
        /// 	constructor on the object.</exception>
        public object AddNew(Type serviceType)
        {
            return AddNew(serviceType, serviceType);
        }

        /// <summary>
        /// 	Creates a service.
        /// </summary>
        /// <param name = "serviceType">The type of the service to create.</param>
        /// <param name = "registerAs">The type the service is registered under.</param>
        /// <returns>The new service instance.</returns>
        /// <exception cref = "ArgumentException">Object builder cannot find an appropriate
        /// 	constructor on the object.</exception>
        public object AddNew(Type serviceType, Type registerAs)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            Guard.ArgumentNotNull(registerAs, "registerAs");
            return Build(serviceType, registerAs, null);
        }

        #endregion

        #region Build

        private object Build(Type typeToBuild, Type typeToRegisterAs, object serviceInstance)
        {
            Guard.TypeIsAssignableFromType(typeToBuild, typeToRegisterAs, "typeToBuild");
            if (_locator.Contains(new DependencyResolutionLocatorKey(typeToRegisterAs, null), SearchMode.Local))
            {
                throw new ArgumentException(string.Format(CultureInfo.CurrentCulture, Resources.DuplicateService, typeToRegisterAs.FullName));
            }
            if (serviceInstance == null)
            {
                serviceInstance = BuildFirstTimeItem(typeToBuild, typeToRegisterAs, null);
            }
            else if (!_container.Contains(serviceInstance))
            {
                serviceInstance = BuildFirstTimeItem(typeToBuild, typeToRegisterAs, serviceInstance);
            }
            else
            {
                BuildRepeatedItem(typeToRegisterAs, serviceInstance);
            }
            return serviceInstance;
        }

        /// <summary>
        /// 	Used to build a first time item (either an existing one or a new one). The Builder will
        /// 	end up locating it, and we add it to the container.
        /// </summary>
        private object BuildFirstTimeItem(Type typeToBuild, Type typeToRegisterAs, object item)
        {
            item = _builder.BuildUp(_locator, typeToBuild, null, item);
            if (typeToRegisterAs != typeToBuild)
            {
                _locator.Add(new DependencyResolutionLocatorKey(typeToRegisterAs, null), item);
                _locator.Remove(new DependencyResolutionLocatorKey(typeToBuild, null));
            }
            OnAdded(item);
            return item;
        }

        /// <summary>
        /// 	Used to "build" an item we've already seen once. We don't use the builder, because that
        /// 	would do double-injection. Since it's already in the lifetime container, all we need to
        /// 	do is add a second locator registration for it for the right name.
        /// </summary>
        private void BuildRepeatedItem(Type typeToRegisterAs, object item)
        {
            _locator.Add(new DependencyResolutionLocatorKey(typeToRegisterAs, null), item);
        }

        #endregion

        #region Contains

        /// <summary>
        /// 	Determines whether the given service type exists in the collection.
        /// </summary>
        /// <typeparam name = "TService">Type of service to search for.</typeparam>
        /// <returns>
        /// 	true if the TService exists; 
        /// 	false otherwise.
        /// </returns>
        public bool Contains<TService>()
        {
            return Contains(typeof (TService));
        }

        /// <summary>
        /// 	Determines whether the given service type exists in the collection.
        /// </summary>
        /// <param name = "serviceType">Type of service to search for.</param>
        /// <returns>
        /// 	true if the serviceType exists; 
        /// 	false otherwise.
        /// </returns>
        public bool Contains(Type serviceType)
        {
            return Contains(serviceType, SearchMode.Up);
        }

        /// <summary>
        /// 	Determines whether the given service type exists directly in the local collection. The parent
        /// 	collections are not consulted.
        /// </summary>
        /// <param name = "serviceType">Type of service to search for.</param>
        /// <returns>
        /// 	true if the serviceType exists; 
        /// 	false otherwise.
        /// </returns>
        public bool ContainsLocal(Type serviceType)
        {
            return Contains(serviceType, SearchMode.Local);
        }

        private bool Contains(Type serviceType, SearchMode searchMode)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            return _locator.Contains(new DependencyResolutionLocatorKey(serviceType, null), searchMode);
        }

        #endregion

        #region Get

        /// <summary>
        /// 	Gets a service.
        /// </summary>
        /// <typeparam name = "TService">The type of the service to be found.</typeparam>
        /// <returns>The service instance, if present; null otherwise.</returns>
        public TService Get<TService>()
        {
            return (TService) Get(typeof (TService), false);
        }

        /// <summary>
        /// 	Gets a service.
        /// </summary>
        /// <typeparam name = "TService">The type of the service to be found.</typeparam>
        /// <param name = "ensureExists">If true, will throw an exception if the service is not found.</param>
        /// <returns>The service instance, if present; null if not (and ensureExists is false).</returns>
        /// <exception cref = "ServiceMissingException">Thrown if ensureExists is true and the service is not found.</exception>
        public TService Get<TService>(bool ensureExists)
        {
            return (TService) Get(typeof (TService), ensureExists);
        }

        /// <summary>
        /// 	Gets a service.
        /// </summary>
        /// <param name = "serviceType">The type of the service to be found.</param>
        /// <returns>The service instance, if present; null otherwise.</returns>
        public object Get(Type serviceType)
        {
            return Get(serviceType, false);
        }

        /// <summary>
        /// 	Gets a service.
        /// </summary>
        /// <param name = "serviceType">The type of the service to be found.</param>
        /// <param name = "ensureExists">If true, will throw an exception if the service is not found.</param>
        /// <returns>The service instance, if present; null if not (and ensureExists is false).</returns>
        /// <exception cref = "ServiceMissingException">Thrown if ensureExists is true and the service is not found.</exception>
        public object Get(Type serviceType, bool ensureExists)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            if (Contains(serviceType, SearchMode.Local))
            {
                var serviceInstance = _locator.Get(new DependencyResolutionLocatorKey(serviceType, null));
                var placeholder = serviceInstance as DemandAddPlaceholder;
                if (placeholder != null)
                {
                    Remove(serviceType);
                    serviceInstance = Build(placeholder.TypeToCreate, serviceType, null);
                }
                return serviceInstance;
            }
            if (_parentCollection != null)
            {
                return _parentCollection.Get(serviceType, ensureExists);
            }
            if (ensureExists)
            {
                throw new ServiceMissingException(serviceType);
            }
            return null;
        }

        #endregion

        #region Remove

        /// <summary>
        /// 	Removes a service registration from the <see cref = "WorkItem" />.
        /// </summary>
        /// <typeparam name = "TService">The service type to remove.</typeparam>
        public void Remove<TService>()
        {
            Remove(typeof (TService));
        }

        /// <summary>
        /// 	Removes a service registration from the <see cref = "WorkItem" />.
        /// </summary>
        /// <param name = "serviceType">The service type to remove.</param>
        public void Remove(Type serviceType)
        {
            Guard.ArgumentNotNull(serviceType, "serviceType");
            var key = new DependencyResolutionLocatorKey(serviceType, null);
            if (_locator.Contains(key, SearchMode.Local))
            {
                var serviceInstance = _locator.Get(key, SearchMode.Local);
                _locator.Remove(new DependencyResolutionLocatorKey(serviceType, null));
                var isLastInstance = _locator.All(pair => !ReferenceEquals(pair.Value, serviceInstance));
                if (isLastInstance)
                {
                    _builder.TearDown(_locator, serviceInstance);
                    _container.Remove(serviceInstance);
                    if (!(serviceInstance is DemandAddPlaceholder))
                    {
                        OnRemoved(serviceInstance);
                    }
                }
            }
        }

        #endregion

        #region DemandAddPlaceholder Class

        private class DemandAddPlaceholder
        {
            #region Fields & Properties

            private readonly Type _typeToCreate;

            public Type TypeToCreate
            {
                get
                {
                    return _typeToCreate;
                }
            }

            #endregion

            #region Constructors

            public DemandAddPlaceholder(Type typeToCreate)
            {
                _typeToCreate = typeToCreate;
            }

            #endregion
        }

        #endregion
    }
}